import {AnimationAction} from './AnimationAction.js'
import {EventDispatcher} from '../core/EventDispatcher.js'
import {LinearInterpolant} from '../math/interpolants/LinearInterpolant.js'
import {PropertyBinding} from './PropertyBinding.js'
import {PropertyMixer} from './PropertyMixer.js'
import {AnimationClip} from './AnimationClip.js'
import {NormalAnimationBlendMode} from '../constants.js'

function AnimationMixer(root) {
  this._root = root
  this._initMemoryManager()
  this._accuIndex = 0

  this.time = 0

  this.timeScale = 1.0
}

AnimationMixer.prototype = Object.assign(Object.create(EventDispatcher.prototype), {
  constructor: AnimationMixer,

  _bindAction: function (action, prototypeAction) {
    const root = action._localRoot || this._root,
      tracks = action._clip.tracks,
      nTracks = tracks.length,
      bindings = action._propertyBindings,
      interpolants = action._interpolants,
      rootUuid = root.uuid,
      bindingsByRoot = this._bindingsByRootAndName

    let bindingsByName = bindingsByRoot[rootUuid]

    if (bindingsByName === undefined) {
      bindingsByName = {}
      bindingsByRoot[rootUuid] = bindingsByName
    }

    for (let i = 0; i !== nTracks; ++i) {
      const track = tracks[i],
        trackName = track.name

      let binding = bindingsByName[trackName]

      if (binding !== undefined) {
        bindings[i] = binding
      } else {
        binding = bindings[i]

        if (binding !== undefined) {
          // existing binding, make sure the cache knows

          if (binding._cacheIndex === null) {
            ++binding.referenceCount
            this._addInactiveBinding(binding, rootUuid, trackName)
          }

          continue
        }

        const path = prototypeAction && prototypeAction._propertyBindings[i].binding.parsedPath

        binding = new PropertyMixer(PropertyBinding.create(root, trackName, path), track.ValueTypeName, track.getValueSize())

        ++binding.referenceCount
        this._addInactiveBinding(binding, rootUuid, trackName)

        bindings[i] = binding
      }

      interpolants[i].resultBuffer = binding.buffer
    }
  },

  _activateAction: function (action) {
    if (!this._isActiveAction(action)) {
      if (action._cacheIndex === null) {
        // this action has been forgotten by the cache, but the user
        // appears to be still using it -> rebind

        const rootUuid = (action._localRoot || this._root).uuid,
          clipUuid = action._clip.uuid,
          actionsForClip = this._actionsByClip[clipUuid]

        this._bindAction(action, actionsForClip && actionsForClip.knownActions[0])

        this._addInactiveAction(action, clipUuid, rootUuid)
      }

      const bindings = action._propertyBindings

      // increment reference counts / sort out state
      for (let i = 0, n = bindings.length; i !== n; ++i) {
        const binding = bindings[i]

        if (binding.useCount++ === 0) {
          this._lendBinding(binding)
          binding.saveOriginalState()
        }
      }

      this._lendAction(action)
    }
  },

  _deactivateAction: function (action) {
    if (this._isActiveAction(action)) {
      const bindings = action._propertyBindings

      // decrement reference counts / sort out state
      for (let i = 0, n = bindings.length; i !== n; ++i) {
        const binding = bindings[i]

        if (--binding.useCount === 0) {
          binding.restoreOriginalState()
          this._takeBackBinding(binding)
        }
      }

      this._takeBackAction(action)
    }
  },

  // Memory manager

  _initMemoryManager: function () {
    this._actions = [] // 'nActiveActions' followed by inactive ones
    this._nActiveActions = 0

    this._actionsByClip = {}
    // inside:
    // {
    // 	knownActions: Array< AnimationAction > - used as prototypes
    // 	actionByRoot: AnimationAction - lookup
    // }

    this._bindings = [] // 'nActiveBindings' followed by inactive ones
    this._nActiveBindings = 0

    this._bindingsByRootAndName = {} // inside: Map< name, PropertyMixer >

    this._controlInterpolants = [] // same game as above
    this._nActiveControlInterpolants = 0

    const scope = this

    this.stats = {
      actions: {
        get total() {
          return scope._actions.length
        },
        get inUse() {
          return scope._nActiveActions
        },
      },
      bindings: {
        get total() {
          return scope._bindings.length
        },
        get inUse() {
          return scope._nActiveBindings
        },
      },
      controlInterpolants: {
        get total() {
          return scope._controlInterpolants.length
        },
        get inUse() {
          return scope._nActiveControlInterpolants
        },
      },
    }
  },

  // Memory management for AnimationAction objects

  _isActiveAction: function (action) {
    const index = action._cacheIndex
    return index !== null && index < this._nActiveActions
  },

  _addInactiveAction: function (action, clipUuid, rootUuid) {
    const actions = this._actions,
      actionsByClip = this._actionsByClip

    let actionsForClip = actionsByClip[clipUuid]

    if (actionsForClip === undefined) {
      actionsForClip = {
        knownActions: [action],
        actionByRoot: {},
      }

      action._byClipCacheIndex = 0

      actionsByClip[clipUuid] = actionsForClip
    } else {
      const knownActions = actionsForClip.knownActions

      action._byClipCacheIndex = knownActions.length
      knownActions.push(action)
    }

    action._cacheIndex = actions.length
    actions.push(action)

    actionsForClip.actionByRoot[rootUuid] = action
  },

  _removeInactiveAction: function (action) {
    const actions = this._actions,
      lastInactiveAction = actions[actions.length - 1],
      cacheIndex = action._cacheIndex

    lastInactiveAction._cacheIndex = cacheIndex
    actions[cacheIndex] = lastInactiveAction
    actions.pop()

    action._cacheIndex = null

    const clipUuid = action._clip.uuid,
      actionsByClip = this._actionsByClip,
      actionsForClip = actionsByClip[clipUuid],
      knownActionsForClip = actionsForClip.knownActions,
      lastKnownAction = knownActionsForClip[knownActionsForClip.length - 1],
      byClipCacheIndex = action._byClipCacheIndex

    lastKnownAction._byClipCacheIndex = byClipCacheIndex
    knownActionsForClip[byClipCacheIndex] = lastKnownAction
    knownActionsForClip.pop()

    action._byClipCacheIndex = null

    const actionByRoot = actionsForClip.actionByRoot,
      rootUuid = (action._localRoot || this._root).uuid

    delete actionByRoot[rootUuid]

    if (knownActionsForClip.length === 0) {
      delete actionsByClip[clipUuid]
    }

    this._removeInactiveBindingsForAction(action)
  },

  _removeInactiveBindingsForAction: function (action) {
    const bindings = action._propertyBindings

    for (let i = 0, n = bindings.length; i !== n; ++i) {
      const binding = bindings[i]

      if (--binding.referenceCount === 0) {
        this._removeInactiveBinding(binding)
      }
    }
  },

  _lendAction: function (action) {
    // [ active actions |  inactive actions  ]
    // [  active actions >| inactive actions ]
    //                 s        a
    //                  <-swap->
    //                 a        s

    const actions = this._actions,
      prevIndex = action._cacheIndex,
      lastActiveIndex = this._nActiveActions++,
      firstInactiveAction = actions[lastActiveIndex]

    action._cacheIndex = lastActiveIndex
    actions[lastActiveIndex] = action

    firstInactiveAction._cacheIndex = prevIndex
    actions[prevIndex] = firstInactiveAction
  },

  _takeBackAction: function (action) {
    // [  active actions  | inactive actions ]
    // [ active actions |< inactive actions  ]
    //        a        s
    //         <-swap->
    //        s        a

    const actions = this._actions,
      prevIndex = action._cacheIndex,
      firstInactiveIndex = --this._nActiveActions,
      lastActiveAction = actions[firstInactiveIndex]

    action._cacheIndex = firstInactiveIndex
    actions[firstInactiveIndex] = action

    lastActiveAction._cacheIndex = prevIndex
    actions[prevIndex] = lastActiveAction
  },

  // Memory management for PropertyMixer objects

  _addInactiveBinding: function (binding, rootUuid, trackName) {
    const bindingsByRoot = this._bindingsByRootAndName,
      bindings = this._bindings

    let bindingByName = bindingsByRoot[rootUuid]

    if (bindingByName === undefined) {
      bindingByName = {}
      bindingsByRoot[rootUuid] = bindingByName
    }

    bindingByName[trackName] = binding

    binding._cacheIndex = bindings.length
    bindings.push(binding)
  },

  _removeInactiveBinding: function (binding) {
    const bindings = this._bindings,
      propBinding = binding.binding,
      rootUuid = propBinding.rootNode.uuid,
      trackName = propBinding.path,
      bindingsByRoot = this._bindingsByRootAndName,
      bindingByName = bindingsByRoot[rootUuid],
      lastInactiveBinding = bindings[bindings.length - 1],
      cacheIndex = binding._cacheIndex

    lastInactiveBinding._cacheIndex = cacheIndex
    bindings[cacheIndex] = lastInactiveBinding
    bindings.pop()

    delete bindingByName[trackName]

    if (Object.keys(bindingByName).length === 0) {
      delete bindingsByRoot[rootUuid]
    }
  },

  _lendBinding: function (binding) {
    const bindings = this._bindings,
      prevIndex = binding._cacheIndex,
      lastActiveIndex = this._nActiveBindings++,
      firstInactiveBinding = bindings[lastActiveIndex]

    binding._cacheIndex = lastActiveIndex
    bindings[lastActiveIndex] = binding

    firstInactiveBinding._cacheIndex = prevIndex
    bindings[prevIndex] = firstInactiveBinding
  },

  _takeBackBinding: function (binding) {
    const bindings = this._bindings,
      prevIndex = binding._cacheIndex,
      firstInactiveIndex = --this._nActiveBindings,
      lastActiveBinding = bindings[firstInactiveIndex]

    binding._cacheIndex = firstInactiveIndex
    bindings[firstInactiveIndex] = binding

    lastActiveBinding._cacheIndex = prevIndex
    bindings[prevIndex] = lastActiveBinding
  },

  // Memory management of Interpolants for weight and time scale

  _lendControlInterpolant: function () {
    const interpolants = this._controlInterpolants,
      lastActiveIndex = this._nActiveControlInterpolants++

    let interpolant = interpolants[lastActiveIndex]

    if (interpolant === undefined) {
      interpolant = new LinearInterpolant(new Float32Array(2), new Float32Array(2), 1, this._controlInterpolantsResultBuffer)

      interpolant.__cacheIndex = lastActiveIndex
      interpolants[lastActiveIndex] = interpolant
    }

    return interpolant
  },

  _takeBackControlInterpolant: function (interpolant) {
    const interpolants = this._controlInterpolants,
      prevIndex = interpolant.__cacheIndex,
      firstInactiveIndex = --this._nActiveControlInterpolants,
      lastActiveInterpolant = interpolants[firstInactiveIndex]

    interpolant.__cacheIndex = firstInactiveIndex
    interpolants[firstInactiveIndex] = interpolant

    lastActiveInterpolant.__cacheIndex = prevIndex
    interpolants[prevIndex] = lastActiveInterpolant
  },

  _controlInterpolantsResultBuffer: new Float32Array(1),

  // return an action for a clip optionally using a custom root target
  // object (this method allocates a lot of dynamic memory in case a
  // previously unknown clip/root combination is specified)
  clipAction: function (clip, optionalRoot, blendMode) {
    const root = optionalRoot || this._root,
      rootUuid = root.uuid

    let clipObject = typeof clip === 'string' ? AnimationClip.findByName(root, clip) : clip

    const clipUuid = clipObject !== null ? clipObject.uuid : clip

    const actionsForClip = this._actionsByClip[clipUuid]
    let prototypeAction = null

    if (blendMode === undefined) {
      if (clipObject !== null) {
        blendMode = clipObject.blendMode
      } else {
        blendMode = NormalAnimationBlendMode
      }
    }

    if (actionsForClip !== undefined) {
      const existingAction = actionsForClip.actionByRoot[rootUuid]

      if (existingAction !== undefined && existingAction.blendMode === blendMode) {
        return existingAction
      }

      // we know the clip, so we don't have to parse all
      // the bindings again but can just copy
      prototypeAction = actionsForClip.knownActions[0]

      // also, take the clip from the prototype action
      if (clipObject === null) clipObject = prototypeAction._clip
    }

    // clip must be known when specified via string
    if (clipObject === null) return null

    // allocate all resources required to run it
    const newAction = new AnimationAction(this, clipObject, optionalRoot, blendMode)

    this._bindAction(newAction, prototypeAction)

    // and make the action known to the memory manager
    this._addInactiveAction(newAction, clipUuid, rootUuid)

    return newAction
  },

  // get an existing action
  existingAction: function (clip, optionalRoot) {
    const root = optionalRoot || this._root,
      rootUuid = root.uuid,
      clipObject = typeof clip === 'string' ? AnimationClip.findByName(root, clip) : clip,
      clipUuid = clipObject ? clipObject.uuid : clip,
      actionsForClip = this._actionsByClip[clipUuid]

    if (actionsForClip !== undefined) {
      return actionsForClip.actionByRoot[rootUuid] || null
    }

    return null
  },

  // deactivates all previously scheduled actions
  stopAllAction: function () {
    const actions = this._actions,
      nActions = this._nActiveActions

    for (let i = nActions - 1; i >= 0; --i) {
      actions[i].stop()
    }

    return this
  },

  // advance the time and update apply the animation
  update: function (deltaTime) {
    deltaTime *= this.timeScale

    const actions = this._actions,
      nActions = this._nActiveActions,
      time = (this.time += deltaTime),
      timeDirection = Math.sign(deltaTime),
      accuIndex = (this._accuIndex ^= 1)

    // run active actions

    for (let i = 0; i !== nActions; ++i) {
      const action = actions[i]

      action._update(time, deltaTime, timeDirection, accuIndex)
    }

    // update scene graph

    const bindings = this._bindings,
      nBindings = this._nActiveBindings

    for (let i = 0; i !== nBindings; ++i) {
      bindings[i].apply(accuIndex)
    }

    return this
  },

  // Allows you to seek to a specific time in an animation.
  setTime: function (timeInSeconds) {
    this.time = 0 // Zero out time attribute for AnimationMixer object;
    for (let i = 0; i < this._actions.length; i++) {
      this._actions[i].time = 0 // Zero out time attribute for all associated AnimationAction objects.
    }

    return this.update(timeInSeconds) // Update used to set exact time. Returns "this" AnimationMixer object.
  },

  // return this mixer's root target object
  getRoot: function () {
    return this._root
  },

  // free all resources specific to a particular clip
  uncacheClip: function (clip) {
    const actions = this._actions,
      clipUuid = clip.uuid,
      actionsByClip = this._actionsByClip,
      actionsForClip = actionsByClip[clipUuid]

    if (actionsForClip !== undefined) {
      // note: just calling _removeInactiveAction would mess up the
      // iteration state and also require updating the state we can
      // just throw away

      const actionsToRemove = actionsForClip.knownActions

      for (let i = 0, n = actionsToRemove.length; i !== n; ++i) {
        const action = actionsToRemove[i]

        this._deactivateAction(action)

        const cacheIndex = action._cacheIndex,
          lastInactiveAction = actions[actions.length - 1]

        action._cacheIndex = null
        action._byClipCacheIndex = null

        lastInactiveAction._cacheIndex = cacheIndex
        actions[cacheIndex] = lastInactiveAction
        actions.pop()

        this._removeInactiveBindingsForAction(action)
      }

      delete actionsByClip[clipUuid]
    }
  },

  // free all resources specific to a particular root target object
  uncacheRoot: function (root) {
    const rootUuid = root.uuid,
      actionsByClip = this._actionsByClip

    for (const clipUuid in actionsByClip) {
      const actionByRoot = actionsByClip[clipUuid].actionByRoot,
        action = actionByRoot[rootUuid]

      if (action !== undefined) {
        this._deactivateAction(action)
        this._removeInactiveAction(action)
      }
    }

    const bindingsByRoot = this._bindingsByRootAndName,
      bindingByName = bindingsByRoot[rootUuid]

    if (bindingByName !== undefined) {
      for (const trackName in bindingByName) {
        const binding = bindingByName[trackName]
        binding.restoreOriginalState()
        this._removeInactiveBinding(binding)
      }
    }
  },

  // remove a targeted clip from the cache
  uncacheAction: function (clip, optionalRoot) {
    const action = this.existingAction(clip, optionalRoot)

    if (action !== null) {
      this._deactivateAction(action)
      this._removeInactiveAction(action)
    }
  },
})

export {AnimationMixer}
